a tool for shared writing and social publishing
at update/reader 145 lines 5.2 kB view raw
1import { subscribeToPublication } from "app/lish/subscribeToPublication"; 2import { cookies } from "next/headers"; 3import { redirect } from "next/navigation"; 4import { NextRequest, NextResponse } from "next/server"; 5import { createOauthClient } from "src/atproto-oauth"; 6import { setAuthToken } from "src/auth"; 7 8import { supabaseServerClient } from "supabase/serverClient"; 9import { URLSearchParams } from "url"; 10import { 11 ActionAfterSignIn, 12 parseActionFromSearchParam, 13} from "./afterSignInActions"; 14import { inngest } from "app/api/inngest/client"; 15 16type OauthRequestClientState = { 17 redirect: string | null; 18 action: ActionAfterSignIn | null; 19}; 20 21export async function GET( 22 req: NextRequest, 23 props: { params: Promise<{ route: string; handle?: string }> }, 24) { 25 const params = await props.params; 26 let client = await createOauthClient(); 27 switch (params.route) { 28 case "metadata": 29 return NextResponse.json(client.clientMetadata); 30 case "jwks": 31 return NextResponse.json(client.jwks); 32 case "login": { 33 const searchParams = req.nextUrl.searchParams; 34 const handle = searchParams.get("handle") as string; 35 // Put originating page here! 36 let redirect = searchParams.get("redirect_url"); 37 if (redirect) redirect = decodeURIComponent(redirect); 38 let action = parseActionFromSearchParam(searchParams.get("action")); 39 let state: OauthRequestClientState = { redirect, action }; 40 41 // Revoke any pending authentication requests if the connection is closed (optional) 42 const ac = new AbortController(); 43 44 const url = await client.authorize(handle || "https://bsky.social", { 45 scope: 46 "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview blob:*/*", 47 signal: ac.signal, 48 state: JSON.stringify(state), 49 }); 50 51 return NextResponse.redirect(url); 52 } 53 case "callback": { 54 const params = new URLSearchParams(req.url.split("?")[1]); 55 56 let redirectPath = "/"; 57 try { 58 const { session, state } = await client.callback(params); 59 let s: OauthRequestClientState = JSON.parse(state || "{}"); 60 redirectPath = decodeURIComponent(s.redirect || "/"); 61 let { data: identity } = await supabaseServerClient 62 .from("identities") 63 .select() 64 .eq("atp_did", session.did) 65 .single(); 66 if (!identity) { 67 let existingIdentity = (await cookies()).get("auth_token"); 68 if (existingIdentity) { 69 let data = await supabaseServerClient 70 .from("email_auth_tokens") 71 .select("*, identities(*)") 72 .eq("id", existingIdentity.value) 73 .single(); 74 if (data.data?.identity && data.data.confirmed) 75 await supabaseServerClient 76 .from("identities") 77 .update({ atp_did: session.did }) 78 .eq("id", data.data.identity); 79 80 return handleAction(s.action, redirectPath); 81 } 82 const { data } = await supabaseServerClient 83 .from("identities") 84 .insert({ atp_did: session.did }) 85 .select() 86 .single(); 87 identity = data; 88 } 89 90 // Trigger migration if identity needs it 91 const metadata = identity?.metadata as Record<string, unknown> | null; 92 if (metadata?.needsStandardSiteMigration) { 93 if (process.env.NODE_ENV === "production") 94 await inngest.send({ 95 name: "user/migrate-to-standard", 96 data: { did: session.did }, 97 }); 98 } 99 100 let { data: token } = await supabaseServerClient 101 .from("email_auth_tokens") 102 .insert({ 103 identity: identity!.id, 104 confirmed: true, 105 confirmation_code: "", 106 }) 107 .select() 108 .single(); 109 console.log({ token }); 110 if (token) await setAuthToken(token.id); 111 112 // Process successful authentication here 113 console.log("authorize() was called with state:", state); 114 115 console.log("User authenticated as:", session.did); 116 return handleAction(s.action, redirectPath); 117 } catch (e) { 118 console.log(e); 119 redirect(redirectPath); 120 } 121 } 122 default: 123 return NextResponse.json({ error: "Invalid route" }, { status: 404 }); 124 } 125} 126 127const handleAction = async ( 128 action: ActionAfterSignIn | null, 129 redirectPath: string, 130) => { 131 let parsePath = decodeURIComponent(redirectPath); 132 let url; 133 if (parsePath.includes("://")) url = new URL(parsePath); 134 else url = new URL(decodeURIComponent(redirectPath), "https://example.com"); 135 if (action?.action === "subscribe") { 136 let result = await subscribeToPublication(action.publication); 137 if (result.success && result.hasFeed === false) 138 url.searchParams.set("showSubscribeSuccess", "true"); 139 } 140 141 let path = url.pathname; 142 if (url.search) path += url.search; 143 if (url.hash) path += url.hash; 144 return parsePath.includes("://") ? redirect(url.toString()) : redirect(path); 145};